# dot+ Base — Design System v1.1
**Inspired by Uber Base · SwiftUI Token Specification**

- **Version:** 1.1 (revised for Base parity)
- **Date:** May 2026
- **Framework:** SwiftUI (iOS 17+)
- **Inspiration:** Uber Base Design System
- **Changelog from v1.0:** Restructured tokens into primitive + semantic layers · Added Light theme · Replaced Manrope with Inter · Recalibrated accent hexes · Fixed button heights, sizes, and shapes · Removed 9pt Micro tier · Marked dot+ extensions explicitly

---

## 1. Design Principles

### 1.1 Foundation Over Decoration
Every visual decision serves a functional purpose. No decorative gradients, no glass blur, no shadows for aesthetics. **Color signals state. Shape signals interactivity. Weight signals hierarchy.**

### 1.2 Mono Foundation, Themeable
The palette is built on a near-black/near-white mono scale. Accent colors exist only to communicate meaning. The system supports **both Light and Dark themes** via semantic tokens — Dark is the primary mode for dot+ at launch; Light is a one-token swap, not a rewrite.

### 1.3 Dense Information, Not Cluttered
Base-inspired density: more information per screen than typical iOS apps, but never cluttered. 4pt grid, tight spacing, intentional whitespace. Users are professionals — they want data density, not lifestyle spacing.

### 1.4 SwiftUI-Native
Every token maps directly to a SwiftUI modifier. No custom drawing unless absolutely necessary. Leverage the system: SF Symbols (as icon vocabulary), Dynamic Type, VoiceOver, haptics.

### 1.5 Accessibility Is Non-Negotiable
- Minimum text contrast **4.5:1** for body, **3:1** for large text (≥18pt or ≥14pt bold)
- Minimum hit target **44×44pt** for every interactive element
- All components support Dynamic Type and VoiceOver labels
- Focus rings on every interactive element (`accent` color, 2pt outset, 2pt stroke)

---

## 2. Color Tokens — Two-Layer Architecture

dot+ Base uses **two layers** of color tokens:

- **Primitive tokens** — raw palette values. Never reference these directly in components.
- **Semantic tokens** — reference primitives. **Always use these in components.** Swapping the semantic→primitive mapping is what enables Light/Dark theming.

### 2.1 Primitive Tokens — Mono Scale

| Token | Hex | Notes |
|---|---|---|
| `mono.0` | `#FFFFFF` | Pure white |
| `mono.50` | `#FAFAFC` | Off-white |
| `mono.100` | `#F2F2F5` | |
| `mono.200` | `#E6E6EB` | |
| `mono.300` | `#C8C8D4` | |
| `mono.400` | `#9494A3` | |
| `mono.500` | `#6B6B78` | |
| `mono.600` | `#2E2E38` | |
| `mono.700` | `#222229` | |
| `mono.800` | `#18181F` | |
| `mono.900` | `#0F0F14` | |
| `mono.1000` | `#05050A` | Near-black |

### 2.2 Primitive Tokens — Accents (Recalibrated)

| Token | Hex | Note |
|---|---|---|
| `accent.positive` | `#05944F` | Matches Base's positive green (cooler than v1.0's `#00A850`) |
| `accent.warning` | `#BB7A00` | Matches Base's warning amber (slightly muted vs. v1.0) |
| `accent.negative` | `#E11900` | Matches Base's negative red (more saturated than v1.0's `#C53030`) |
| `accent.info` | `#276EF1` | Matches Base's accent blue |

### 2.3 Semantic Tokens — Dark Theme (Primary)

| Token | → Primitive | Usage |
|---|---|---|
| `surface.primary` | `mono.900` | Screen background |
| `surface.secondary` | `mono.800` | Cards, elevated panels |
| `surface.tertiary` | `mono.700` | Inputs, nested surfaces |
| `surface.inverse` | `mono.50` | Inverted surfaces (e.g. primary CTAs) |
| `surface.overlay` | `mono.1000` @ 60% | Modal scrim |
| `content.primary` | `mono.0` | Body text, titles |
| `content.secondary` | `mono.300` | Supporting text, meta |
| `content.tertiary` | `mono.500` | Labels, timestamps, captions |
| `content.disabled` | `mono.600` | Disabled text, placeholders |
| `content.inverse` | `mono.1000` | Text on inverse surfaces |
| `border.opaque` | `mono.600` | Hairlines, dividers |
| `border.subtle` | `mono.700` | Internal row dividers |
| `state.positive` | `accent.positive` | Success states |
| `state.warning` | `accent.warning` | Pending/queued states |
| `state.negative` | `accent.negative` | Errors, recording indicator |
| `state.info` | `accent.info` | Links, badges, AI labels |

### 2.4 Semantic Tokens — Light Theme

| Token | → Primitive |
|---|---|
| `surface.primary` | `mono.0` |
| `surface.secondary` | `mono.50` |
| `surface.tertiary` | `mono.100` |
| `surface.inverse` | `mono.1000` |
| `content.primary` | `mono.1000` |
| `content.secondary` | `mono.600` |
| `content.tertiary` | `mono.500` |
| `content.disabled` | `mono.300` |
| `content.inverse` | `mono.0` |
| `border.opaque` | `mono.200` |
| `border.subtle` | `mono.100` |
| (state.* unchanged) | | |

> **Components reference only `surface.*`, `content.*`, `border.*`, `state.*`.** They never know if they're in Light or Dark mode.

---

## 3. Typography

### 3.1 Type Family
- **Display & UI:** **Inter** (neutral grotesque, free, near-Uber-Move proportions). Fallback: SF Pro.
- **Mono:** **JetBrains Mono** or **Inter Mono**. Fallback: SF Mono.

> Manrope was replaced because it reads as geometric/friendly; Inter is the closest free analogue to Uber Move's tone.

### 3.2 Type Scale

| Token | Font | Size | Weight | Line Height | Usage |
|---|---|---|---|---|---|
| `type.display` | Inter | 32 | Bold (700) | 36 | Screen titles |
| `type.headline` | Inter | 22 | Bold (700) | 28 | Section titles, card headers |
| `type.title` | Inter | 18 | SemiBold (600) | 24 | Nav bar center, meeting title |
| `type.body` | Inter | 15 | Regular (400) | 22 | Body text, descriptions |
| `type.bodyStrong` | Inter | 15 | SemiBold (600) | 22 | Emphasized body |
| `type.label` | Inter | 13 | Medium (500) | 18 | List rows, button labels |
| `type.caption` | JetBrains Mono | 11 | Regular (400) | 14 | Metadata, timestamps |

> **Removed:** the 9pt "Micro" tier — below comfortable read at 1× and fails Dynamic Type. Use `type.caption` at 11pt as the floor; for ALL-CAPS section headers, use `type.label` with `tracking: 1.5`.

> **Display weight reduced** from ExtraBold (800) to Bold (700) — Uber Move's largest register is never extra-heavy.

### 3.3 SwiftUI Implementation
```swift
extension Font {
    static let dotDisplay    = Font.custom("Inter-Bold",       size: 32)
    static let dotHeadline   = Font.custom("Inter-Bold",       size: 22)
    static let dotTitle      = Font.custom("Inter-SemiBold",   size: 18)
    static let dotBody       = Font.custom("Inter-Regular",    size: 15)
    static let dotBodyStrong = Font.custom("Inter-SemiBold",   size: 15)
    static let dotLabel      = Font.custom("Inter-Medium",     size: 13)
    static let dotCaption    = Font.custom("JetBrainsMono-Regular", size: 11)
}
```

---

## 4. Spacing & Layout

### 4.1 Grid
4pt base grid. All spacing values are multiples of 4.

| Token | Value | Usage |
|---|---|---|
| `space.1` | 4pt | Icon ↔ label gap |
| `space.2` | 8pt | Within-component padding |
| `space.3` | 12pt | Default card padding, button padding |
| `space.4` | 16pt | Screen edge margin, between cards |
| `space.5` | 20pt | Section spacing |
| `space.6` | 24pt | Major section breaks |
| `space.7` | 32pt | Above section headers |
| `space.8` | 48pt | Top of screen padding |

### 4.2 Corner Radius

| Token | Value | Usage |
|---|---|---|
| `radius.xs` | 4pt | Badges, pills, micro |
| `radius.sm` | 8pt | Cards, inputs, buttons, toggles |
| `radius.md` | 12pt | Bottom sheet cards |
| `radius.lg` | 16pt | Full-screen sheet handle (ceiling) |

> Never use `radius > 16`. dot+ is functional, not playful.

### 4.3 Elevation
- **Dark theme:** elevation is shown by **surface tonal lift** (`surface.primary` → `surface.secondary` → `surface.tertiary`). No drop shadow.
- **Light theme:** elevation uses real shadow tokens:
  - `shadow.1` — `0 1px 2px rgba(0,0,0,0.08)` (cards)
  - `shadow.2` — `0 4px 12px rgba(0,0,0,0.10)` (popovers, dropdowns)
  - `shadow.3` — `0 12px 32px rgba(0,0,0,0.16)` (modals)

---

## 5. Core Components

### 5.1 `DotButton`

**4 shapes:** `default` (radius.sm), `pill` (fully rounded), `square` (radius 0), `circle` (icon-only).

**4 sizes:**

| Size | Height | Horizontal padding | Font |
|---|---|---|---|
| `mini` | 28pt | 12pt | `type.label` |
| `small` | 36pt | 14pt | `type.label` |
| `default` | 48pt | 16pt | `type.bodyStrong` |
| `large` | 56pt | 20pt | `type.bodyStrong` |

> **Fix from v1.0:** v1.0 listed "14px height" which was a typo. Default is **48pt**.

**Variants:**
- **Primary:** `surface.inverse` fill, `content.inverse` text. No border.
- **Secondary:** `surface.tertiary` fill, `content.primary` text. **No border** (tinted, not outlined — this matches Base's secondary).
- **Tertiary / Ghost:** transparent fill, `content.primary` text, `border.opaque` 1pt border.
- **Destructive:** `state.negative` fill, `mono.0` text.

**Behavior:** minimum hit target 44×44pt always (even at `mini`/`small` — extend tap area). Haptic `.light` on tap.

### 5.2 `DotCard`
- Fill: `surface.secondary`
- Border: **none** (Base default — fill-only on dark; 1pt `border.subtle` on Light)
- Radius: `radius.sm`
- Padding: `space.3`
- Internal row dividers: 1pt `border.subtle`

> **Fix from v1.0:** removed the simultaneous fill + border treatment. Pick one. Default = fill only.

### 5.3 `DotTag` *(renamed from DotPill; matches Base nomenclature)*

**Default variant:** `surface.tertiary` fill, `content.primary` text, no border. This is the canonical Base treatment.

**Semantic variants** use solid tinted fills:
- `positive` — `accent.positive` @ 16% fill, `accent.positive` text
- `warning` — `accent.warning` @ 16% fill, `accent.warning` text
- `negative` — `accent.negative` @ 16% fill, `accent.negative` text
- `info` — `accent.info` @ 16% fill, `accent.info` text

> Drop the "1px @ 20% border" — Base tags are fill-only. Cleaner.

Size: 24pt height, 8pt horizontal padding, `radius.xs`, `type.caption`.

### 5.4 `DotInput`
- Height: 48pt
- Fill: `surface.tertiary`
- Border: 1pt transparent, becomes 1pt `state.info` on focus, 1pt `state.negative` on error
- Radius: `radius.sm`
- Label above (12pt, `content.secondary`, `type.label`)
- Caption below (11pt, `content.tertiary`, `type.caption`) — used for hint or error message

### 5.5 `DotToggle`
- Track: 42×24pt, `radius.xs` (pill)
- On: `surface.inverse` track, `surface.primary` knob
- Off: `border.opaque` track, `surface.primary` knob
- Knob: 20×20pt
- Animation: 0.15s ease

### 5.6 *(dot+ extension)* `DotTabBar`
> Not a Base component — Base has no bottom tab bar. This is a dot+ extension following Apple HIG.

- Height: 80pt · Background: `surface.primary` (solid, no blur) · Border-top: 1pt `border.subtle`
- Active icon: full brightness SF Symbol · Inactive: `content.tertiary` (40% opacity equivalent)
- Active label: `type.caption`, `content.primary` · Inactive: `type.caption`, `content.tertiary`

### 5.7 `DotNavBar`
- Height: 52pt · Background: `surface.primary` (solid, no blur)
- Title: `type.title`, center or leading
- Back: `content.secondary`, "← Label"
- Trailing: `content.primary`, `type.bodyStrong` or SF Symbol

### 5.8 *(dot+ extension)* `DotPillStatus`
A vertically-stacked status indicator unique to dot+ (recording, processing, etc.) — not a Base pattern. Documented in `Components / dot+ Extensions`.

---

## 6. Iconography

- **Primary set:** SF Symbols (24pt grid, semibold weight by default). Aligns with iOS-native feel.
- **Custom icons:** when SF Symbols can't cover a concept, draw on 24pt grid, 1.5pt stroke, rounded caps & joins (Base's icon contract).
- **Touch target around icon:** always 44pt minimum.

---

## 7. Motion & Interaction

- Default animation: `.easeInOut`, 0.15s — state changes (toggle, button press)
- Sheet presentation: `.easeOut`, 0.3s — bottom sheet, modal
- Tab switch: no animation
- Haptics: `.light` on tap; `.medium` on record start/stop; `.success` on analysis complete
- **No spring animations**
- Loading: 1pt border spinner, 0.8s linear rotation. Never skeleton screens.

---

## 8. Base ↔ dot+ Boundary

To honor Base's "Extension Library" governance model, every component is classified:

| Class | Components |
|---|---|
| **Core (Base-aligned)** | `DotButton`, `DotCard`, `DotTag`, `DotInput`, `DotToggle`, `DotNavBar` |
| **Extension (iOS / dot+ specific)** | `DotTabBar`, `DotPillStatus`, any custom dot+ visuals |
| **One-off (discouraged)** | None at launch — must be reviewed before adding |

Anything new must justify why a Core component can't cover it.

---

## 9. Figma File Structure *(unchanged from v1.0)*

Page layout, naming convention, and frame sizes carry over from v1.0 — see Section 7 of the original spec. Add **Page 0 — Tokens (Primitive)** before existing Page 2.

### 9.1 Figma Variables (revised)
- **Color collection:** `Primitive` (mono + accents) + `Semantic / Dark` + `Semantic / Light`
- **Mode-switching:** semantic collection has Dark + Light modes; components only bind to semantic variables
- **Number collection:** `space.*` (4, 8, 12, 16, 20, 24, 32, 48), `radius.*` (4, 8, 12, 16)
- **String collection:** font families, font weights

This lets a Figma user toggle the whole file Light↔Dark from one switch, and lets the SwiftUI `DesignTokens.swift` mirror the same structure 1:1.
